ποΈGitΠ―ΡΠ°ποΈ
Commit 1fdc9214896723f4be94aaa5c1d8527d2d1b04fa
Parents : 80d2ad2
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-08-13T15:53:50Z
Committer : GitHub <noreply@github.com>
Date : 2026-08-13T15:53:50Z
fix(ui): move event firmware branding off the app bar (#6676)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Changes
7 files changed, 136 insertions(+), 37 deletions(-)
Diff
diff --git a/.skills/compose-ui/strings-index.txt b/.skills/compose-ui/strings-index.txt
index 7f2c8efb93..44874dfd48 100644
--- a/.skills/compose-ui/strings-index.txt
+++ b/.skills/compose-ui/strings-index.txt
@@ -582,6 +582,7 @@ establishing_session
ethernet_config
ethernet_enabled
ethernet_ip
+event_firmware_running
event_use_event_theme
exchange_position
expand_chart
diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml
index 5370b0112c..d4d4c6bbbd 100644
--- a/core/resources/src/commonMain/composeResources/values/strings.xml
+++ b/core/resources/src/commonMain/composeResources/values/strings.xml
@@ -609,6 +609,7 @@
<string name="ethernet_config">Ethernet Options</string>
<string name="ethernet_enabled">Ethernet enabled</string>
<string name="ethernet_ip">Ethernet IP:</string>
+ <string name="event_firmware_running">Running event firmware</string>
<string name="event_use_event_theme">Use event theme</string>
<string name="exchange_position">Exchange position</string>
<string name="expand_chart">Expand chart</string>
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EventInfoSheet.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EventInfoSheet.kt
index 3dfa00f756..f93872ffc9 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EventInfoSheet.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EventInfoSheet.kt
@@ -69,8 +69,9 @@ import org.meshtastic.core.ui.util.brandPalette
import org.meshtastic.core.ui.util.safeLinks
/**
- * Bottom sheet shown when the user taps the event branding in [MainAppBar]. Surfaces the event metadata the bundled
- * `event_firmware.json` carries β welcome message, location, dates, and links β themed with the edition's accent color.
+ * Bottom sheet shown when the user taps the event card on the Connections screen. Surfaces the event metadata the
+ * bundled `event_firmware.json` carries β welcome message, location, dates, and links β themed with the edition's
+ * accent color.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MainAppBar.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MainAppBar.kt
index 41ee09382e..79aa59d90d 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MainAppBar.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MainAppBar.kt
@@ -19,12 +19,9 @@ package org.meshtastic.core.ui.component
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
@@ -34,14 +31,8 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.compositeOver
-import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
@@ -53,8 +44,6 @@ import org.meshtastic.core.resources.navigate_back
import org.meshtastic.core.ui.icon.ArrowBack
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.theme.LocalEventTheme
-import org.meshtastic.core.ui.util.EventBrandingIcon
-import org.meshtastic.core.ui.util.LocalEventBranding
/** Alpha for the ambient event accent wash over the app bar β subtle enough to keep title text legible. */
private const val EVENT_ACCENT_ALPHA = 0.12f
@@ -72,9 +61,9 @@ fun MainAppBar(
showNodeChip: Boolean,
canNavigateUp: Boolean,
onNavigateUp: () -> Unit,
- actions: @Composable () -> Unit,
onClickChip: (Node) -> Unit,
- brandingContent: @Composable () -> Unit = { EventAwareBranding() },
+ // Trailing slot: a @Composable content lambda, not an event handler (detekt LambdaParameterEventTrailing).
+ actions: @Composable () -> Unit,
) {
// Ambient event theming: when connected to event firmware (and not opted out), tint the bar with a faint wash of
// the edition's accent color. Gated with the app-wide fonts via LocalEventTheme / the "Use event theme" toggle.
@@ -122,7 +111,9 @@ fun MainAppBar(
}
}
} else {
- { brandingContent() }
+ // The Meshtastic logo is never swapped for event branding β the app's identity stays put. Event
+ // firmware is surfaced on the Connections screen instead (EventFirmwareCard).
+ { Icon(imageVector = vectorResource(Res.drawable.ic_meshtastic), contentDescription = null) }
},
actions = {
TopBarActions(
@@ -137,24 +128,6 @@ fun MainAppBar(
}
}
-/** Reads [LocalEventBranding] to show event branding (tap β [EventInfoSheet]), or the default Meshtastic logo. */
-@Composable
-private fun EventAwareBranding() {
- val eventEdition = LocalEventBranding.current
- if (eventEdition == null) {
- Icon(imageVector = vectorResource(Res.drawable.ic_meshtastic), contentDescription = null)
- return
- }
- // Every event edition is tappable for its info sheet. The icon prefers the hosted iconUrl, then a bundled
- // drawable, then the Meshtastic logo β see EventBrandingIcon.
- var showSheet by remember { mutableStateOf(false) }
- val brandingModifier = Modifier.size(32.dp).clip(CircleShape).clickable(role = Role.Button) { showSheet = true }
- EventBrandingIcon(edition = eventEdition, modifier = brandingModifier)
- if (showSheet) {
- EventInfoSheet(edition = eventEdition, onDismiss = { showSheet = false })
- }
-}
-
@Composable
private fun TopBarActions(
ourNode: Node?,
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalEventBranding.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalEventBranding.kt
index f02bbfb7dd..019630641d 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalEventBranding.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/LocalEventBranding.kt
@@ -40,9 +40,10 @@ import kotlin.time.Clock
/**
* Provides the active [EventFirmwareEdition] (if any) to the composition tree. When a connected device reports an event
- * firmware edition, this local is populated at the app root so that
- * [MainAppBar][org.meshtastic.core.ui.component.MainAppBar] can display event branding automatically β no per-screen
- * wiring needed.
+ * firmware edition, this local is populated at the app root so consumers can pick it up without per-screen wiring: the
+ * Connections screen surfaces the edition itself, and [LocalEventTheme][org.meshtastic.core.ui.theme.LocalEventTheme]
+ * derives the ambient theme from it. The app-bar logo is deliberately *not* one of those consumers β the Meshtastic
+ * identity stays in place regardless of the firmware running on the device.
*/
@Suppress("CompositionLocalAllowlist")
val LocalEventBranding = compositionLocalOf<EventFirmwareEdition?> { null }
diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt
index d5896d4809..a44ab9cef7 100644
--- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt
+++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt
@@ -115,6 +115,7 @@ import org.meshtastic.feature.connections.ui.components.ConnectingDeviceInfo
import org.meshtastic.feature.connections.ui.components.CurrentlyConnectedInfo
import org.meshtastic.feature.connections.ui.components.CurrentlyConnectedText
import org.meshtastic.feature.connections.ui.components.DeviceList
+import org.meshtastic.feature.connections.ui.components.EventFirmwareCard
import org.meshtastic.feature.connections.ui.components.TransportSelector
import org.meshtastic.feature.settings.navigation.ConfigRoute
import org.meshtastic.feature.settings.navigation.getNavRouteFrom
@@ -340,6 +341,18 @@ fun ConnectionsScreen(
}
}
+ // Event firmware is reported here rather than by swapping the app-bar logo: the Meshtastic
+ // identity stays put, and the edition reads as one more fact about the connected device.
+ // LocalEventBranding is only populated while connected to event firmware, so the card comes
+ // and goes with the device. Hidden once the event is over β the ended-event card below takes
+ // over from here, and celebrating an event that has passed would undercut its nudge.
+ LocalEventBranding.current
+ ?.takeIf { !it.hasEnded() }
+ ?.let { edition ->
+ Spacer(modifier = Modifier.height(8.dp))
+ EventFirmwareCard(edition = edition)
+ }
+
firmwareUpdateNotice?.let { notice ->
FirmwareUpdateNoticeCard(
notice = notice,
diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/EventFirmwareCard.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/EventFirmwareCard.kt
new file mode 100644
index 0000000000..54db25743e
--- /dev/null
+++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/EventFirmwareCard.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.feature.connections.ui.components
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import org.jetbrains.compose.resources.stringResource
+import org.meshtastic.core.model.EventFirmwareEdition
+import org.meshtastic.core.resources.Res
+import org.meshtastic.core.resources.event_firmware_running
+import org.meshtastic.core.ui.component.EventInfoSheet
+import org.meshtastic.core.ui.component.EventPaletteStrip
+import org.meshtastic.core.ui.icon.ChevronRight
+import org.meshtastic.core.ui.icon.MeshtasticIcons
+import org.meshtastic.core.ui.util.EventBrandingIcon
+import org.meshtastic.core.ui.util.brandPalette
+
+/** Size of the event icon in the card β matches the node avatar in the connected-device card above it. */
+private val EVENT_ICON_SIZE = 40.dp
+
+/** Closing brand rule under the card, thicker than the app-bar hairline since this is the branding's home. */
+private val PALETTE_STRIP_HEIGHT = 4.dp
+
+/**
+ * Card announcing that the connected device is running an event firmware edition, and the entry point to
+ * [EventInfoSheet] for that event's welcome message, venue, dates, and links.
+ *
+ * This is where event branding lives: the app bar keeps the Meshtastic logo, so the edition is reported alongside the
+ * other facts about the connected device rather than replacing the app's own identity.
+ *
+ * The caller is responsible for hiding this once the event has ended
+ * ([hasEnded][org.meshtastic.core.ui.util.hasEnded]); a finished event gets the "return to standard firmware" card
+ * instead.
+ */
+@Composable
+fun EventFirmwareCard(edition: EventFirmwareEdition, modifier: Modifier = Modifier) {
+ var showSheet by remember { mutableStateOf(false) }
+ Card(modifier = modifier.fillMaxWidth()) {
+ Row(
+ modifier = Modifier.fillMaxWidth().clickable(role = Role.Button) { showSheet = true }.padding(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ // Prefers the hosted iconUrl, then a bundled drawable, then the Meshtastic logo β see EventBrandingIcon.
+ EventBrandingIcon(
+ edition = edition,
+ modifier = Modifier.size(EVENT_ICON_SIZE).clip(CircleShape),
+ contentDescription = null,
+ )
+ Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
+ Text(
+ text = edition.displayName,
+ style = MaterialTheme.typography.titleMedium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ Text(
+ text = stringResource(Res.string.event_firmware_running),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ Icon(
+ imageVector = MeshtasticIcons.ChevronRight,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ EventPaletteStrip(palette = edition.brandPalette(), height = PALETTE_STRIP_HEIGHT)
+ }
+ if (showSheet) {
+ EventInfoSheet(edition = edition, onDismiss = { showSheet = false })
+ }
+}
Served by rngit 1.5.0 - Generated in 0.1s